package DyingBaby.backend.dm;

import com.google.common.primitives.Bytes;
import DyingBaby.backend.common.SubArray;
import DyingBaby.backend.dm.dataItem.DataItem;
import DyingBaby.backend.dm.logger.Logger;
import DyingBaby.backend.dm.page.Page;
import DyingBaby.backend.dm.page.PageX;
import DyingBaby.backend.dm.pageCache.PageCache;
import DyingBaby.backend.tm.TransactionManager;
import DyingBaby.utils.Panic;
import DyingBaby.utils.Parser;

import java.util.*;
import java.util.Map.Entry;

/**
 * 数据库恢复模块
 * 负责在数据库崩溃后，通过日志文件进行数据恢复
 * 实现基于WAL(Write-Ahead Logging)的恢复机制
 */
public class Recover {

    // 日志类型常量
    private static final byte LOG_TYPE_INSERT = 0;  // 插入日志类型
    private static final byte LOG_TYPE_UPDATE = 1;  // 更新日志类型

    // 恢复操作类型常量
    private static final int REDO = 0;  // 重做操作
    private static final int UNDO = 1;  // 撤销操作

    /**
     * 插入日志信息结构
     */
    static class InsertLogInfo {
        long xid;      // 事务ID
        int pgno;      // 页号
        short offset;  // 页内偏移量
        byte[] raw;    // 原始数据
    }

    /**
     * 更新日志信息结构
     */
    static class UpdateLogInfo {
        long xid;       // 事务ID
        int pgno;       // 页号
        short offset;   // 页内偏移量
        byte[] oldRaw;  // 旧数据
        byte[] newRaw;  // 新数据
    }

    /**
     * 主恢复方法
     * @param tm 事务管理器
     * @param lg 日志记录器
     * @param pc 页面缓存
     */
    public static void recover(TransactionManager tm, Logger lg, PageCache pc) {
        System.out.println("Recovering...");

        // 重置日志读取位置到开头
        lg.rewind();
        int maxPgno = 0;

        // 遍历所有日志记录，找到最大的页面号
        while(true) {
            byte[] log = lg.next();
            if(log == null) break;
            int pgno;
            if(isInsertLog(log)) {
                InsertLogInfo li = parseInsertLog(log);
                pgno = li.pgno;
            } else {
                UpdateLogInfo li = parseUpdateLog(log);
                pgno = li.pgno;
            }
            if(pgno > maxPgno) {
                maxPgno = pgno;
            }
        }

        // 如果没有找到任何页面，至少保留一个页面
        if(maxPgno == 0) {
            maxPgno = 1;
        }

        // 截断页面缓存，移除超出最大页面号的部分
        pc.truncateByBgno(maxPgno);
        System.out.println("Truncate to " + maxPgno + " pages.");

        // 重做所有已提交的事务
        redoTranscations(tm, lg, pc);
        System.out.println("Redo Transactions Over.");

        // 撤销所有未提交的事务
        undoTranscations(tm, lg, pc);
        System.out.println("Undo Transactions Over.");

        System.out.println("Recovery Over.");
    }

    /**
     * 重做已提交的事务
     * @param tm 事务管理器
     * @param lg 日志记录器
     * @param pc 页面缓存
     */
    private static void redoTranscations(TransactionManager tm, Logger lg, PageCache pc) {
        lg.rewind();
        while(true) {
            byte[] log = lg.next();
            if(log == null) break;
            if(isInsertLog(log)) {
                InsertLogInfo li = parseInsertLog(log);
                long xid = li.xid;
                // 只重做非活跃事务（已提交的事务）
                if(!tm.isActive(xid)) {
                    doInsertLog(pc, log, REDO);
                }
            } else {
                UpdateLogInfo xi = parseUpdateLog(log);
                long xid = xi.xid;
                // 只重做非活跃事务（已提交的事务）
                if(!tm.isActive(xid)) {
                    doUpdateLog(pc, log, REDO);
                }
            }
        }
    }

    /**
     * 撤销未提交的事务
     * @param tm 事务管理器
     * @param lg 日志记录器
     * @param pc 页面缓存
     */
    private static void undoTranscations(TransactionManager tm, Logger lg, PageCache pc) {
        // 按事务ID缓存日志记录
        Map<Long, List<byte[]>> logCache = new HashMap<>();
        lg.rewind();

        // 收集所有活跃事务的日志记录
        while(true) {
            byte[] log = lg.next();
            if(log == null) break;
            if(isInsertLog(log)) {
                InsertLogInfo li = parseInsertLog(log);
                long xid = li.xid;
                if(tm.isActive(xid)) {
                    if(!logCache.containsKey(xid)) {
                        logCache.put(xid, new ArrayList<>());
                    }
                    logCache.get(xid).add(log);
                }
            } else {
                UpdateLogInfo xi = parseUpdateLog(log);
                long xid = xi.xid;
                if(tm.isActive(xid)) {
                    if(!logCache.containsKey(xid)) {
                        logCache.put(xid, new ArrayList<>());
                    }
                    logCache.get(xid).add(log);
                }
            }
        }

        // 对所有活跃事务的日志进行倒序撤销（后发生的操作先撤销）
        for(Entry<Long, List<byte[]>> entry : logCache.entrySet()) {
            List<byte[]> logs = entry.getValue();
            for (int i = logs.size()-1; i >= 0; i --) {
                byte[] log = logs.get(i);
                if(isInsertLog(log)) {
                    doInsertLog(pc, log, UNDO);
                } else {
                    doUpdateLog(pc, log, UNDO);
                }
            }
            // 标记事务为已中止
            tm.abort(entry.getKey());
        }
    }

    /**
     * 判断日志是否为插入类型
     * @param log 日志字节数组
     * @return 是否为插入日志
     */
    private static boolean isInsertLog(byte[] log) {
        return log[0] == LOG_TYPE_INSERT;
    }

    // 日志格式说明:
    // insertLog格式:
    // [LogType] [XID] [Pgno] [Offset] [Raw]
    // updateLog格式:
    // [LogType] [XID] [UID] [OldRaw] [NewRaw]

    // 日志字段偏移量定义
    private static final int OF_TYPE = 0;                    // 日志类型偏移
    private static final int OF_XID = OF_TYPE+1;             // 事务ID偏移
    private static final int OF_UPDATE_UID = OF_XID+8;       // 更新日志UID偏移
    private static final int OF_UPDATE_RAW = OF_UPDATE_UID+8; // 更新日志数据偏移

    /**
     * 创建更新日志
     * @param xid 事务ID
     * @param di 数据项
     * @return 日志字节数组
     */
    public static byte[] updateLog(long xid, DataItem di) {
        byte[] logType = {LOG_TYPE_UPDATE};
        byte[] xidRaw = Parser.long2Byte(xid);
        byte[] uidRaw = Parser.long2Byte(di.getUid());
        byte[] oldRaw = di.getOldRaw();
        SubArray raw = di.getRaw();
        byte[] newRaw = Arrays.copyOfRange(raw.raw, raw.start, raw.end);
        return Bytes.concat(logType, xidRaw, uidRaw, oldRaw, newRaw);
    }

    /**
     * 解析更新日志
     * @param log 日志字节数组
     * @return 更新日志信息
     */
    private static UpdateLogInfo parseUpdateLog(byte[] log) {
        UpdateLogInfo li = new UpdateLogInfo();
        li.xid = Parser.parseLong(Arrays.copyOfRange(log, OF_XID, OF_UPDATE_UID));
        long uid = Parser.parseLong(Arrays.copyOfRange(log, OF_UPDATE_UID, OF_UPDATE_RAW));
        // 从UID中解析偏移量（低16位）
        li.offset = (short)(uid & ((1L << 16) - 1));
        uid >>>= 32;
        // 从UID中解析页号（高32位）
        li.pgno = (int)(uid & ((1L << 32) - 1));
        int length = (log.length - OF_UPDATE_RAW) / 2;
        li.oldRaw = Arrays.copyOfRange(log, OF_UPDATE_RAW, OF_UPDATE_RAW+length);
        li.newRaw = Arrays.copyOfRange(log, OF_UPDATE_RAW+length, OF_UPDATE_RAW+length*2);
        return li;
    }

    /**
     * 执行更新日志操作
     * @param pc 页面缓存
     * @param log 日志字节数组
     * @param flag 操作类型（REDO/UNDO）
     */
    private static void doUpdateLog(PageCache pc, byte[] log, int flag) {
        int pgno;
        short offset;
        byte[] raw;
        if(flag == REDO) {
            // 重做操作：使用新数据
            UpdateLogInfo xi = parseUpdateLog(log);
            pgno = xi.pgno;
            offset = xi.offset;
            raw = xi.newRaw;
        } else {
            // 撤销操作：使用旧数据
            UpdateLogInfo xi = parseUpdateLog(log);
            pgno = xi.pgno;
            offset = xi.offset;
            raw = xi.oldRaw;
        }
        Page pg = null;
        try {
            pg = pc.getPage(pgno);
        } catch (Exception e) {
            Panic.panic(e);
        }
        try {
            // 执行页面更新恢复
            PageX.recoverUpdate(pg, raw, offset);
        } finally {
            pg.release();
        }
    }

    // 插入日志字段偏移量定义
    private static final int OF_INSERT_PGNO = OF_XID+8;     // 插入日志页号偏移
    private static final int OF_INSERT_OFFSET = OF_INSERT_PGNO+4; // 插入日志偏移量偏移
    private static final int OF_INSERT_RAW = OF_INSERT_OFFSET+2;  // 插入日志数据偏移

    /**
     * 创建插入日志
     * @param xid 事务ID
     * @param pg 页面
     * @param raw 原始数据
     * @return 日志字节数组
     */
    public static byte[] insertLog(long xid, Page pg, byte[] raw) {
        byte[] logTypeRaw = {LOG_TYPE_INSERT};
        byte[] xidRaw = Parser.long2Byte(xid);
        byte[] pgnoRaw = Parser.int2Byte(pg.getPageNumber());
        byte[] offsetRaw = Parser.short2Byte(PageX.getFSO(pg));
        return Bytes.concat(logTypeRaw, xidRaw, pgnoRaw, offsetRaw, raw);
    }

    /**
     * 解析插入日志
     * @param log 日志字节数组
     * @return 插入日志信息
     */
    private static InsertLogInfo parseInsertLog(byte[] log) {
        InsertLogInfo li = new InsertLogInfo();
        li.xid = Parser.parseLong(Arrays.copyOfRange(log, OF_XID, OF_INSERT_PGNO));
        li.pgno = Parser.parseInt(Arrays.copyOfRange(log, OF_INSERT_PGNO, OF_INSERT_OFFSET));
        li.offset = Parser.parseShort(Arrays.copyOfRange(log, OF_INSERT_OFFSET, OF_INSERT_RAW));
        li.raw = Arrays.copyOfRange(log, OF_INSERT_RAW, log.length);
        return li;
    }

    /**
     * 执行插入日志操作
     * @param pc 页面缓存
     * @param log 日志字节数组
     * @param flag 操作类型（REDO/UNDO）
     */
    private static void doInsertLog(PageCache pc, byte[] log, int flag) {
        InsertLogInfo li = parseInsertLog(log);
        Page pg = null;
        try {
            pg = pc.getPage(li.pgno);
        } catch(Exception e) {
            Panic.panic(e);
        }
        try {
            if(flag == UNDO) {
                // 撤销操作：标记数据为无效
                DataItem.setDataItemRawInvalid(li.raw);
            }
            // 执行页面插入恢复
            PageX.recoverInsert(pg, li.raw, li.offset);
        } finally {
            pg.release();
        }
    }
}